﻿using System;
using System.Collections.Generic;
using System.Linq;
using Windows.Media.Capture;
using Windows.Media.Devices;
using Windows.Media.MediaProperties;

namespace Microsoft.Media.CaptureClient
{
    /// <summary>
    /// Provides a way to automatically create default input and/or output profiles based on your device's native capabilities.
    /// For example, if your device only records at 480p, building 720p video tracks will only slow down your machine.
    /// </summary>
    public static class EncodingProfileHelper
    {
        static readonly VideoEncodingQuality[] defaultVideoTrackQualities = { VideoEncodingQuality.HD1080p, VideoEncodingQuality.HD720p, VideoEncodingQuality.Vga, VideoEncodingQuality.Qvga };
        
        /// <summary>
        /// Creates default profiles and initializes the capture session. 
        /// Note: you can also set the CaptureEncodingProfile, VideoOutputEncodingProperties, and AudioOutputEncodingProperties properties directly on the CaptureSession object instead of calling this method.
        /// </summary>
        /// <param name="captureSession">The CaptureSession object to initialize.</param>
        /// <param name="audioDeviceController">The properties of the video input source to base the profiles on.</param>
        /// <param name="videoDeviceController">The properties of the video input source to base the profiles on.</param>
        /// <param name="videoEncodingPreferences">Indicates how the profile should be constructed (e.g. multi or single bitrate).</param>
        /// <param name="maxVerticalResolution">Provides a cap on the vertical resolution. If set, width will be determined by aspect ratio of referenceVideoProperties</param>
        public static void InitializeCaptureSession(CaptureSession captureSession, AudioDeviceController audioDeviceController, VideoDeviceController videoDeviceController, VideoEncodingPreferences videoEncodingPreferences, uint? maxVerticalResolution)
        {
            var profiles = GetDefaultProfiles(audioDeviceController, videoDeviceController, videoEncodingPreferences, maxVerticalResolution);
            captureSession.CaptureEncodingProfile = profiles.InputEncodingProfile;
            captureSession.VideoOutputEncodingProperties = profiles.OutputEncodingProfiles.OfType<VideoEncodingProperties>().ToList();
            captureSession.AudioOutputEncodingProperties = profiles.OutputEncodingProfiles.OfType<AudioEncodingProperties>().ToList();
        }

        /// <summary>
        /// Call to automatically create input and output profiles based on the supplied device native capabilities.
        /// </summary>
        /// <param name="audioDeviceController">The properties of the video input source to base the profiles on.</param>
        /// <param name="videoDeviceController">The properties of the video input source to base the profiles on.</param>
        /// <param name="videoEncodingPreferences">Indicates how the profile should be constructed (e.g. multi or single bitrate).</param>
        /// <param name="maxVerticalResolution">Provides a cap on the vertical resolution. If set, width will be determined by aspect ratio of referenceVideoProperties</param>
        public static CaptureSessionProfiles GetDefaultProfiles(AudioDeviceController audioDeviceController, VideoDeviceController videoDeviceController, VideoEncodingPreferences videoEncodingPreferences, uint? maxVerticalResolution)
        {
            AudioEncodingProperties audioProps = null;
            VideoEncodingProperties videoProps = null;

            // try to get properties from the devices being used to record audio and video and use them to drive the default encoding profile used.
            try
            {
                audioProps = audioDeviceController.GetMediaStreamProperties(MediaStreamType.Audio) as AudioEncodingProperties;
            }
            catch { /* occurs when no device is found */ }
            try
            {
                videoProps = videoDeviceController.GetMediaStreamProperties(MediaStreamType.VideoRecord) as VideoEncodingProperties;

                if (maxVerticalResolution.HasValue && videoProps.Height > maxVerticalResolution.Value)
                {
                    videoProps = videoDeviceController.GetAvailableMediaStreamProperties(MediaStreamType.VideoRecord)
                        .OfType<VideoEncodingProperties>()
                        .Where(v => v.Subtype == videoProps.Subtype)
                        .OrderByDescending(v => v.Height)
                        .SkipWhile(v => v.Height > maxVerticalResolution.Value)
                        .DefaultIfEmpty(videoProps)
                        .First();
                }
            }
            catch { /* occurs when no device is found */ }

            var inputEncodingProfile = GetDefaultInputProfile(audioProps, videoProps);
            var outputEncodingProfiles = GetDefaultOutputProfiles(videoEncodingPreferences, inputEncodingProfile);

            return new CaptureSessionProfiles(inputEncodingProfile, outputEncodingProfiles);
        }

        /// <summary>
        /// Gets default output encoding profiles
        /// </summary>
        /// <param name="videoEncodingPreferences">Indicates how the profile should be constructed (e.g. multi or single bitrate).</param>
        /// <param name="inputEncodingProfile">Provides the input encoding profile. Profile will typically match audio and video profiles provided by other parameters.</param>
        /// <returns>A collection of encoding profiles to use for output.</returns>
        public static IList<IMediaEncodingProperties> GetDefaultOutputProfiles(VideoEncodingPreferences videoEncodingPreferences, MediaEncodingProfile inputEncodingProfile)
        {
            var result = new List<IMediaEncodingProperties>();

            var qualities = defaultVideoTrackQualities
                .Select(q => new { Quality = q, Height = GetVerticalResolution(q) })
                .OrderBy(qh => qh.Height)
                .ToList();

            var defaultQuality = qualities.Last();
            if (inputEncodingProfile.Video != null)
            {
                defaultQuality = qualities.FirstOrDefault(q => q.Height >= inputEncodingProfile.Video.Height) ?? defaultQuality;
            }
            var defaultProfile = MediaEncodingProfile.CreateMp4(defaultQuality.Quality);

            if (inputEncodingProfile.Video != null)
            { // VIDEO
                // restrictions documented here: http://msdn.microsoft.com/en-us/library/windows/desktop/dd797816(v=vs.85).aspx
                var inputVideoProperties = inputEncodingProfile.Video;
                double aspectRatio = (double)inputVideoProperties.Width / inputVideoProperties.Height;

                var videoTrackProperties = defaultProfile.Video;
                videoTrackProperties.ProfileId = inputVideoProperties.ProfileId;
                videoTrackProperties.FrameRate.Numerator = inputVideoProperties.FrameRate.Numerator;
                videoTrackProperties.FrameRate.Denominator = inputVideoProperties.FrameRate.Denominator;
                videoTrackProperties.PixelAspectRatio.Numerator = inputVideoProperties.PixelAspectRatio.Numerator;
                videoTrackProperties.PixelAspectRatio.Denominator = inputVideoProperties.PixelAspectRatio.Denominator;
                videoTrackProperties.Width = inputVideoProperties.Width;
                videoTrackProperties.Height = inputVideoProperties.Height;
                videoTrackProperties.Bitrate = GetRecommendedBitrate(videoTrackProperties);
                result.Add(videoTrackProperties);

                if (videoEncodingPreferences != VideoEncodingPreferences.SingleBitrate)
                {
                    // add additional tracks for all recommended resolutions smaller than the native resolution
                    foreach (var quality in qualities.TakeWhile(q => q.Height < inputVideoProperties.Height))
                    {
                        var nextVideoTrackProperties = MediaEncodingProfile.CreateMp4(quality.Quality).Video;
                        nextVideoTrackProperties.ProfileId = inputVideoProperties.ProfileId;
                        nextVideoTrackProperties.FrameRate.Numerator = inputVideoProperties.FrameRate.Numerator;
                        nextVideoTrackProperties.FrameRate.Denominator = inputVideoProperties.FrameRate.Denominator;
                        nextVideoTrackProperties.PixelAspectRatio.Numerator = inputVideoProperties.PixelAspectRatio.Numerator;
                        nextVideoTrackProperties.PixelAspectRatio.Denominator = inputVideoProperties.PixelAspectRatio.Denominator;
                        uint qualityWidth = (uint)(aspectRatio * nextVideoTrackProperties.Height);
                        if (videoEncodingPreferences == VideoEncodingPreferences.MultiResolution)
                        {
                            nextVideoTrackProperties.Height = quality.Height;
                            nextVideoTrackProperties.Width = qualityWidth;
                        }
                        else // retain the native width and height but use the scaled width and height to calculate bitrate
                        {
                            nextVideoTrackProperties.Height = inputVideoProperties.Height;
                            nextVideoTrackProperties.Width = inputVideoProperties.Width;
                        }
                        nextVideoTrackProperties.Bitrate = GetRecommendedBitrate(qualityWidth, quality.Height);
                        result.Add(nextVideoTrackProperties);
                    }
                }
            }

            if (inputEncodingProfile.Audio != null)
            { // AUDIO
                // restrictions documented here: http://msdn.microsoft.com/en-us/library/windows/desktop/dd742785(v=vs.85).aspx
                var inputAudioProperties = inputEncodingProfile.Audio;
                var audioTrackProperties = defaultProfile.Audio;
                audioTrackProperties.SampleRate = inputAudioProperties.SampleRate;
                audioTrackProperties.ChannelCount = inputAudioProperties.ChannelCount;
                audioTrackProperties.BitsPerSample = 16;
                uint[] supportedBitrates = new uint[] { 12000 * 8, 16000 * 8, 20000 * 8, 24000 * 8 };
                audioTrackProperties.Bitrate = GetClosestMatch(supportedBitrates, GetRecommendedBitrate(audioTrackProperties));

                result.Add(audioTrackProperties);
            }

            return result;
        }

        /// <summary>
        /// Gets a default input profile based on the native device capabilities.
        /// </summary>
        /// <param name="referenceAudioProperties">Provides a bar to base the audio profile on. Profile returned will never exceed the properties provided.</param>
        /// <param name="referenceVideoProperties">Provides a bar to base the video profiles on. Profile returned will never exceed the properties provided.</param>
        /// <returns>An audio and video profile to use for capturing media.</returns>
        public static MediaEncodingProfile GetDefaultInputProfile(AudioEncodingProperties referenceAudioProperties, VideoEncodingProperties referenceVideoProperties)
        {
            var result = new MediaEncodingProfile();
            result.Container = null;
            result.Video = null;
            result.Audio = null;

            if (referenceVideoProperties != null)
            {  // VIDEO
                // restrictions documented here: http://msdn.microsoft.com/en-us/library/windows/desktop/dd797816(v=vs.85).aspx
                result.Video = VideoEncodingProperties.CreateUncompressed(MediaEncodingSubtypes.Nv12, referenceVideoProperties.Width, referenceVideoProperties.Height);
                result.Video.FrameRate.Numerator = referenceVideoProperties.FrameRate.Numerator;
                result.Video.FrameRate.Denominator = referenceVideoProperties.FrameRate.Denominator;
                result.Video.PixelAspectRatio.Numerator = referenceVideoProperties.PixelAspectRatio.Numerator;
                result.Video.PixelAspectRatio.Denominator = referenceVideoProperties.PixelAspectRatio.Denominator;
            }

            if (referenceAudioProperties != null)
            { // AUDIO
                // restrictions documented here: http://msdn.microsoft.com/en-us/library/windows/desktop/dd742785(v=vs.85).aspx
                uint[] supportedChannelCount = new uint[] { 1, 2 };
                uint channelCount = GetClosestMatch(supportedChannelCount, referenceAudioProperties.ChannelCount);
                uint[] supportedSampleRates = new uint[] { 44100, 48000 };
                uint sampleRate = GetClosestMatch(supportedSampleRates, referenceAudioProperties.SampleRate);
                result.Audio = AudioEncodingProperties.CreatePcm(sampleRate, channelCount, 16);
            }

            return result;
        }

        static uint GetVerticalResolution(VideoEncodingQuality quality)
        {
            switch (quality)
            {
                case VideoEncodingQuality.HD1080p:
                    return 1080;
                case VideoEncodingQuality.HD720p:
                    return 720;
                case VideoEncodingQuality.Pal:
                    return 576;
                case VideoEncodingQuality.Vga:
                case VideoEncodingQuality.Wvga:
                case VideoEncodingQuality.Ntsc:
                    return 480;
                case VideoEncodingQuality.Qvga:
                    return 240;
                default:
                    throw new NotImplementedException();
            }
        }

        static uint GetRecommendedBitrate(AudioEncodingProperties properties)
        {
            const uint baseResolution = 2 * 44100 * 16; // 2 channel 44.1KHz 16bps
            const uint baseBitrate = 96000; // 96Kbps

            uint resolution = properties.ChannelCount * properties.SampleRate * properties.BitsPerSample;

            return 1000 * (resolution * (baseBitrate / 1000) / baseResolution);
        }

        static uint GetRecommendedBitrate(VideoEncodingProperties properties)
        {
            return GetRecommendedBitrate(properties.Width, properties.Height);
        }

        static uint GetRecommendedBitrate(uint width, uint height)
        {
            const uint baseResolution = 1280 * 720; // 720p
            const uint baseBitrate = 2 * 1024 * 1024; // 2Mbps

            uint resolution = width * height;

            return 1000 * (resolution * (baseBitrate / 1000) / baseResolution);
        }

        static uint GetClosestMatch(IEnumerable<uint> collection, uint value)
        {
            return collection.OrderBy(v => Math.Abs((long)v - (long)value)).First();
        }
    }

    /// <summary>
    /// Used to store both input and output profiles to be used with the CaptureSession.
    /// </summary>
    public sealed class CaptureSessionProfiles
    {
        /// <summary>
        /// Creates a new instance of CaptureSessionProfiles with the supplied profiles.
        /// </summary>
        /// <param name="inputEncodingProfile">The profile to be used when capturing from the device. Must be uncompressed PCM and NV12.</param>
        /// <param name="outputEncodingProfiles">The profiles (should contain an AAC audio and at least one H264 video) to encode to.</param>
        internal CaptureSessionProfiles(MediaEncodingProfile inputEncodingProfile, IList<IMediaEncodingProperties> outputEncodingProfiles)
        {
            InputEncodingProfile = inputEncodingProfile;
            OutputEncodingProfiles = outputEncodingProfiles;
        }

        /// <summary>
        /// Gets the input profile to be used with CaptureSession.
        /// </summary>
        public MediaEncodingProfile InputEncodingProfile { get; private set; }

        /// <summary>
        /// Gets the output profiles to be used with CaptureSession.
        /// </summary>
        public IList<IMediaEncodingProperties> OutputEncodingProfiles { get; private set; }
    }
}
